package com.zxj.judge.service.impl;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.command.ListImagesCmd;
import com.github.dockerjava.api.command.PullImageCmd;
import com.github.dockerjava.api.command.PullImageResultCallback;
import com.github.dockerjava.api.command.StatsCmd;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Volume;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
import com.zxj.common.core.contains.Constants;
import com.zxj.common.core.contains.JudgeConstants;
import com.zxj.common.core.enums.CodeRunStatus;
import com.zxj.judge.callback.DockerStartResultCallback;
import com.zxj.judge.callback.StatisticsCallback;
import com.zxj.judge.domain.CompileResult;
import com.zxj.judge.domain.SandBoxExecuteResult;
import com.zxj.judge.service.ISandboxService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 沙箱服务实现类
 */
@Service
@Slf4j
public class SandboxServiceImpl implements ISandboxService {

    @Value("${sandbox.docker.host:tcp://localhost:2375}")
    private String dockerHost; // docker宿主机地址

    @Value("${sandbox.limit.memory:100000000}")
    private Long memoryLimit; // 内存限制

    @Value("${sandbox.limit.memory-swap:100000000}")
    private Long memorySwapLimit; // 内存交换限制

    @Value("${sandbox.limit.cpu:1}")
    private Long cpuLimit; // cpu限制

    @Value("${sandbox.limit.time:5}")
    private Long timeLimit; // 时间限制

    /**
     * docker客户端
     */
    private DockerClient dockerClient;

    /**
     * 容器id
     */
    private String containerId;

    /**
     * 用户代码目录
     */
    private String userCodeDir;

    /**
     * 用户代码文件名
     */
    private String userCodeFileName;

    /**
     * 执行用户代码
     * @param userId 用户id
     * @param userCode 用户代码
     * @param inputList 输入数据
     * @return 执行结果
     */
    @Override
    public SandBoxExecuteResult exeJavaCode(Long userId, String userCode, List<String> inputList) {
        // 1. 创建用户代码的文件
        createUserCodeFile(userId, userCode);
        // 2. 初始化docker沙箱
        initDockerSanBox();
        // 3. 编译代码
        CompileResult compileResult = compileCodeByDocker();
        if (!compileResult.isCompiled()) {
            // 编译失败, 删除容器, 删除用户代码文件, 返回编译失败结果
            deleteContainer();
            deleteUserCodeFile();
            return SandBoxExecuteResult.fail(CodeRunStatus.COMPILE_FAILED, compileResult.getExeMessage());
        }
        //执行代码
        return executeJavaCodeByDocker(inputList);
    }


    /**
     * 创建并返回用户代码的文件
     * @param userId 用户id
     * @param userCode 用户代码
     */
    private void createUserCodeFile(Long userId, String userCode) {
        // 1. 创建存放用户代码的目录
        // D:\tmp\SpringCloud-OJ\zxj-oj + \ + user-code
        String examCodeDir = System.getProperty("user.dir") + File.separator + JudgeConstants.EXAM_CODE_DIR;
        if (!FileUtil.exist(examCodeDir)) {
            FileUtil.mkdir(examCodeDir); //创建存放用户代码的目录
        }
        // 2. 创建用户代码文件
        String time = LocalDateTimeUtil.format(LocalDateTime.now(), DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
        // 拼接用户代码文件格式
        userCodeDir = examCodeDir + File.separator + userId + Constants.UNDERLINE_SEPARATOR + time;
        userCodeFileName = userCodeDir + File.separator + JudgeConstants.USER_CODE_JAVA_CLASS_NAME;
        // 3. 写入用户代码到文件
        FileUtil.writeString(userCode, userCodeFileName, Constants.UTF8);

        // 最终生成的文件路径为：D:\\tmp\\SpringCloud-OJ\\zxj-oj\\user-code\\1_20210803121212\\Solution.java
    }

    /**
     * 初始化docker沙箱
     */
    private void initDockerSanBox() {
        DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerHost)
                .build();
        dockerClient = DockerClientBuilder
                .getInstance(clientConfig)
                .withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
                .build();
        //拉取镜像
        pullJavaEnvImage();
        //创建容器  限制资源   控制权限
        HostConfig hostConfig = getHostConfig();
        CreateContainerCmd containerCmd = dockerClient
                .createContainerCmd(JudgeConstants.JAVA_ENV_IMAGE)
                .withName(JudgeConstants.JAVA_CONTAINER_NAME);
        CreateContainerResponse createContainerResponse = containerCmd
                .withHostConfig(hostConfig)
                .withAttachStderr(true)
                .withAttachStdout(true)
                .withTty(true)
                .exec();
        //记录容器id
        containerId = createContainerResponse.getId();
        //启动容器
        dockerClient.startContainerCmd(containerId).exec();
    }

    /**
     * 拉取java执行环境镜像 需要控制只拉取一次
     */
    private void pullJavaEnvImage() {
        // 执行docker images命令，查看本地镜像列表
        ListImagesCmd listImagesCmd = dockerClient.listImagesCmd();
        List<Image> imageList = listImagesCmd.exec();
        for (Image image : imageList) {
            String[] repoTags = image.getRepoTags();
            if (repoTags != null && repoTags.length > 0 && JudgeConstants.JAVA_ENV_IMAGE.equals(repoTags[0])) {
                // 已经拉取过java执行环境镜像, 直接返回
                return;
            }
        }
        // 拉取java执行环境镜像
        PullImageCmd pullImageCmd = dockerClient.pullImageCmd(JudgeConstants.JAVA_ENV_IMAGE);
        try {
            // 阻塞等待拉取完成 - 死等
            pullImageCmd.exec(new PullImageResultCallback()).awaitCompletion();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 限制资源   控制权限
     * @return HostConfig
     */
    private HostConfig getHostConfig() {
        HostConfig hostConfig = new HostConfig();
        // 设置挂载目录，指定用户代码路径
        hostConfig.setBinds(new Bind(userCodeDir, new Volume(JudgeConstants.DOCKER_USER_CODE_DIR)));
        // 限制docker容器使用资源
        hostConfig.withMemory(memoryLimit);
        hostConfig.withMemorySwap(memorySwapLimit);
        hostConfig.withCpuCount(cpuLimit);
        hostConfig.withNetworkMode("none");  //禁用网络
        hostConfig.withReadonlyRootfs(true); //禁止在root目录写文件
        return hostConfig;
    }

    /**
     * 使用Docker编译代码
     *
     * @return 编译结果，封装在CompileResult对象中
     */
    private CompileResult compileCodeByDocker() {
        // 1. 创建Docker命令以编译用户代码
        // 此处假设createExecCmd方法会根据提供的命令（如Java编译器命令）、null（可能表示无额外参数）和容器ID来生成一个命令ID
        String cmdId = createExecCmd(JudgeConstants.DOCKER_JAVAC_CMD, null, containerId);

        // 2. 初始化Docker执行结果回调对象，用于接收编译过程中的输出和结果
        DockerStartResultCallback resultCallback = new DockerStartResultCallback();

        // 3. 初始化编译结果对象
        CompileResult compileResult = new CompileResult();

        try {
            // 4. 使用Docker客户端执行命令，并等待命令完成
            // execStartCmd 方法用于开始执行之前创建的命令
            // exec 方法会启动命令执行，并传入我们定义的回调对象以接收结果
            // awaitCompletion 方法会阻塞当前线程，直到命令执行完成
            dockerClient.execStartCmd(cmdId)
                    .exec(resultCallback)
                    .awaitCompletion();

            // 5. 根据回调对象中的结果更新编译结果对象
            // 如果执行状态为失败，则设置编译未通过，并保存错误信息
            if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) {
                compileResult.setCompiled(false);
                compileResult.setExeMessage(resultCallback.getErrorMessage());
            } else {
                // 如果执行状态不是失败（可能是成功或其他状态，但此处简化为成功处理），则设置编译通过
                compileResult.setCompiled(true);
            }

            // 6. 返回编译结果对象
            return compileResult;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 使用docker执行代码
     * @param inputList 输入数据
     * @return 执行结果
     */
    private SandBoxExecuteResult executeJavaCodeByDocker(List<String> inputList) {
        try {
            List<String> outList = new ArrayList<>(); //记录输出结果
            long maxMemory = 0L;  //最大占用内存
            long maxUseTime = 0L; //最大运行时间
            //执行用户代码
            for (String inputArgs : inputList) {
                String cmdId = createExecCmd(JudgeConstants.DOCKER_JAVA_EXEC_CMD, inputArgs, containerId);
                //执行代码
                StopWatch stopWatch = new StopWatch();        //执行代码后开始计时
                //执行情况监控
                StatsCmd statsCmd = dockerClient.statsCmd(containerId); //启动监控
                StatisticsCallback statisticsCallback = statsCmd.exec(new StatisticsCallback());
                stopWatch.start();
                DockerStartResultCallback resultCallback = new DockerStartResultCallback();
                try {
                    //执行命令
                    dockerClient.execStartCmd(cmdId)
                            .exec(resultCallback)
                            .awaitCompletion(timeLimit, TimeUnit.SECONDS);
                    if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) {
                        //运行失败, 该用例未通过
                        return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED);
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                stopWatch.stop();  //结束时间统计
                statsCmd.close();  //结束docker容器执行统计
                long userTime = stopWatch.getLastTaskTimeMillis(); //执行耗时
                maxUseTime = Math.max(userTime, maxUseTime);       //记录最大的执行用例耗时
                Long memory = statisticsCallback.getMaxMemory();  //获取该用例最大内存
                if (memory != null) {
                    maxMemory = Math.max(maxMemory, statisticsCallback.getMaxMemory()); //记录最大的执行用例占用内存
                }
                outList.add(resultCallback.getMessage().trim());   //记录正确的输出结果
            }
            return getSanBoxResult(inputList, outList, maxMemory, maxUseTime); //封装结果
        } finally { //无论如何都要删除容器和文件
            deleteContainer();//删除容器
            deleteUserCodeFile(); //清理文件
        }
    }

    /**
     * 创建执行命令
     * @param javaCmdArr java命令
     * @param inputArgs 输入参数
     * @param containerId 容器id
     * @return 执行命令id
     */
    private String createExecCmd(String[] javaCmdArr, String inputArgs, String containerId) {
        if (!StrUtil.isEmpty(inputArgs)) {
            //当入参不为空时拼接入参
            String[] inputArray = inputArgs.split(" "); //入参
            javaCmdArr = ArrayUtil.append(JudgeConstants.DOCKER_JAVA_EXEC_CMD, inputArray);
        }
        ExecCreateCmdResponse cmdResponse = dockerClient
                .execCreateCmd(containerId) // 创建执行命令
                .withCmd(javaCmdArr) // 执行命令
                .withAttachStderr(true) // 是否附加错误输出
                .withAttachStdin(true) // 是否附加输入
                .withAttachStdout(true) // 是否附加输出
                .exec(); // 执行
        return cmdResponse.getId(); // 返回执行命令id
    }

    /**
     * 封装沙箱执行结果
     * @param inputList 输入数据
     * @param outList 输出数据
     * @param maxMemory 最大内存
     * @param maxUseTime 最大运行时间
     * @return 沙箱执行结果
     */
    private SandBoxExecuteResult getSanBoxResult(List<String> inputList, List<String> outList,
                                                 long maxMemory, long maxUseTime) {
        if (inputList.size() != outList.size()) {
            //输入用例数量 不等于 输出用例数量  属于执行异常
            return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED, outList, maxMemory, maxUseTime);
        }
        return SandBoxExecuteResult.success(CodeRunStatus.SUCCEED, outList, maxMemory, maxUseTime);
    }

    /**
     * 删除容器
     */
    private void deleteContainer() {
        //执行完成之后删除容器
        dockerClient.stopContainerCmd(containerId).exec();
        dockerClient.removeContainerCmd(containerId).exec();
        //断开和docker连接
        try {
            dockerClient.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 删除用户代码文件
     */
    private void deleteUserCodeFile() {
        FileUtil.del(userCodeDir);
    }
}
